<?php

// vim: expandtab shiftwidth=2 softtabstop=2 tabstop=2

/**
 * Provide access to database config table
 *
 * PHP version 5
 *
 * @category  Classes
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mconfig.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */

/**
 * Class who provides interface to config database table.
 *
 * Class use PDO's prepared statements.
 *
 * Error codes:
 * 110 - BadFunctionCallException  : Singleton cloning not allowed.
 * 200 - RuntimeException          : ::some problem with database query
 * 210 - OutOfBoundsException      : mConfig property "%s" not found.
 *
 * @category  Classes
 * @package   mPack
 * @author    Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mconfig.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
final class mConfig implements ArrayAccess
{
  /**
   * constant - SELECT prepared statement
   */
  const ST_SEL = 1;

  /**
   * constant - REPLACE prepared statement
   */
  const ST_DB  = 2;

  /**
   * constant - DELETE prepared statement
   */
  const ST_DEL = 4;

  /**
   * Database connection handler
   *
   * @var object
   * @access private
   */
  private $_db            = null;

  /**
   * Database table name
   *
   * @var string
   * @access public
   */
  public $tableName     = 'config';

  /**
   * mConfig object
   *
   * @var object
   * @access private
   */
  private static $_object = null;

  /**
   * Storage for prepared statements
   *
   * @var object
   * @access private
   */
  private $_stmt          = array();

  /**
   * Cache
   *
   * @var array
   * @access private
   */
  private $_cache         = array();

  /**
   * Create mConfig object and/or return it. Singleton init method.
   *
   * @param object $db PDO instance
   * @param boolean $cacheAll if true, get all settings from db at once, and store it in cache
   *
   * @return object mConfig
   * @access public
   */
  public static function init(PDO &$db, $cacheAll=false)
  {
    if (is_null(self::$_object)) {
      self::$_object = new mConfig($db, $cacheAll);
    }
    return self::$_object;
  }

  /**
   * Constructor
   *
   * Creates prepared statements with acquire bindings.
   *
   * @param object $db PDO instance
   * @param boolean $cacheAll if true, get all settings from db at once, and store it in cache
   *
   * @access private
   */
  private function __construct(PDO &$db, $cacheAll)
  {
    $this->_db =& $db;

    $this->_stmt[self::ST_SEL] = $this->_db->prepare(sprintf("
      SELECT
        `value`
      FROM
        %s
      WHERE
        `key` = :key",

      $this->tableName
    ));
    $this->_stmt[self::ST_DB] = $this->_db->prepare(sprintf("
      REPLACE
        %s
      SET
        `key` = :key,
        `value` = :value",

      $this->tableName
    ));
    $this->_stmt[self::ST_DEL] = $this->_db->prepare(sprintf("
      DELETE FROM
        %s
      WHERE
        `key` = :key",

      $this->tableName
    ));



    if ($cacheAll)
    {
      $query = sprintf("
        SELECT
          key,
          value
        FROM
          %s",

          $this->tableName
      );
      try {
        foreach ($this->_db->query($query) as $row) {
          $this->_cache[$row['key']] = unserialize($row['value']);
        }
      } catch (PDOException $e) {
        throw new RuntimeException($e->getMessage(), 200);
      }
      $this->_db->closeCursor();
    }

  }

  /**
   * Overloaded getter
   *
   * Magic: if property name begins with underscore, it will not raise an
   * exception if property will not be found in database, but return false.
   *
   * @param string $key name of property
   *
   * @return mixed value of property or false if not found
   * @throws RuntimeException if any database error
   * @throws OutOfBoundsException if property not found
   *
   * @access public
   */
  public function __get($key)
  {
    //needed setting is in _cache already ?
    if (array_key_exists($key, $this->_cache)) {
      return $this->_cache[$key];
    }



    $silent = ('_' == $key[0]);
    if ($silent) {
      $key = substr($key, 1);
    }

    //shortcut
    $stmt =& $this->_stmt[self::ST_SEL];
    try {
      $stmt->execute(array(':key'=>$key));
    } catch (PDOException $e) {
      throw new RuntimeException($e->getMessage(), 200);
    }

    $row = $stmt->fetch();
    if (!$row) {
      if ($silent) {
        return false;
      } else {
        throw new OutOfBoundsException(sprintf('mConfig property "%s" not found.', $key), 210);
      }
    }
    $stmt->closeCursor();
    $this->_cache[$key] = unserialize($row['value']);

    return $this->_cache[$key];
  }

  /**
   * Overloaded setter
   *
   * @param string $key   name of property
   * @param string $value value of property
   *
   * @throws RuntimeException if any database error
   *
   * @access public
   */
  public function __set($key, $value)
  {
    $s_value = serialize($value);
    //shortcut
    $stmt = &$this->_stmt[self::ST_DB];

    try {
      $stmt->execute(array(
          ':key' => $key,
          ':value' => $s_value
      ));
      $stmt->closeCursor();
    } catch (PDOException $e) {
      throw new RuntimeException($e->getMessage(), 200);
    }

    $this->_cache[$key] = $value;
  }

  /**
   * Overloaded __isset() for checking did property is set
   *
   * @param string $k name of property
   *
   * @return boolean
   * @access public
   */
  public function __isset($key)
  {
    try {
      $this->$key;
      return true;
    } catch (OutOfBoundsException $e) {
      return false;
    }
  }

  /**
   * Overloaded __unset() for deleting properties
   *
   * @param string $key name of property
   *
   * @return boolean
   * @access public
   */
  public function __unset($key)
  {
    if (array_key_exists($key, $this->_cache)) {
      unset($this->_cache[$key]);
    }
    $this->_stmt[self::ST_DEL]->execute(array(':key' => $key));
  }

  /**
   * Overloaded method to forbid cloning of instance
   *
   * @throws BadFunctionCallException always when invoked
   * @access public
   */
  public function __clone()
  {
    throw new BadFunctionCallException('Singleton cloning not allowed.', 110);
  }

  /**
   * Clear cache table
   *
   * @access public
   */
  public function clearCache()
  {
    $this->_cache = array();
  }



  /**
   * Alias to mConfig::__isset()
   *
   * Implements ArrayAccess::offsetExists()
   *
   * @param mixed $offset
   *
   * @return boolean
   * @access public
   */
  public function offsetExists($offset)
  {
    return isset($offset);
  }

  /**
   * Offer array-type acces to data.
   *
   * Implements ArrayAccess::offsetGet()
   *
   * @param mixed $offset
   *
   * @return boolean
   * @access public
   */
  public function offsetGet($offset)
  {
    return $this->$offset;
  }

  /**
   * Offer array-type acces to data.
   *
   * Implements ArrayAccess::offsetSet()
   *
   * @param mixed $offset
   * @param mixed $value
   *
   * @access public
   */
  public function offsetSet($offset, $value)
  {
    $this->$offset = $value;
  }

  /**
   * Offer array-type acces to data.
   *
   * Implements ArrayAccess::offsetUnset()
   *
   * @param mixed $offset
   *
   * @access public
   */
  public function offsetUnset($offset)
  {
    unset($this->$offset);
  }

}

?>
